Schemify
Architecture

Architecture

Module Map

src/
β”œβ”€β”€ main.zig              application entry point (4 dvui callbacks)
β”œβ”€β”€ cli.zig               CLI subcommands (--cli mode)
β”œβ”€β”€ PluginIF.zig          stable plugin ABI definition
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ Schemify.zig      netlist generation, hierarchy
β”‚   β”œβ”€β”€ types.zig         Wire, Instance, Pin, Net, Prop, Conn
β”‚   β”œβ”€β”€ fileio/
β”‚   β”‚   β”œβ”€β”€ Reader.zig    CHN parser
β”‚   β”‚   β”œβ”€β”€ Writer.zig    CHN serializer
β”‚   β”‚   β”œβ”€β”€ Toml.zig      Config.toml parser
β”‚   β”‚   └── utils.zig     file I/O helpers
β”‚   β”œβ”€β”€ devices/
β”‚   β”‚   └── Devices.zig   DeviceKind enum, primitives, PDK models
β”‚   └── simulation/
β”‚       β”œβ”€β”€ SpiceIF.zig   simulator interface, Value, SpiceComponent
β”‚       β”œβ”€β”€ Netlist.zig   SPICE IR, validation, emission
β”‚       └── backend/      ngspice + Xyce runners
β”œβ”€β”€ gui/
β”‚   β”œβ”€β”€ lib.zig           frame dispatcher (render order)
β”‚   β”œβ”€β”€ Input.zig         keyboard/mouse handling
β”‚   β”œβ”€β”€ Actions.zig       keybind β†’ Action enum
β”‚   β”œβ”€β”€ Canvas/
β”‚   β”‚   β”œβ”€β”€ lib.zig
β”‚   β”‚   β”œβ”€β”€ SymbolRenderer.zig
β”‚   β”‚   β”œβ”€β”€ WireRenderer.zig
β”‚   β”‚   β”œβ”€β”€ Interaction.zig    hit-test, drag, rubber-band
β”‚   β”‚   β”œβ”€β”€ SelectionOverlay.zig
β”‚   β”‚   └── TbOverlay.zig     testbench pill button + ghost wires
β”‚   β”œβ”€β”€ Bars/             toolbar, tabbar, command bar
β”‚   β”œβ”€β”€ Panels/           file browser, library, marketplace
β”‚   β”œβ”€β”€ Dialogs/          properties, keybinds, find, spice code
β”‚   β”œβ”€β”€ Keybinds/         keymap definitions
β”‚   └── state/
β”‚       β”œβ”€β”€ AppState.zig  global app state (plugins, config, open tabs)
β”‚       β”œβ”€β”€ Document.zig  per-tab schematic + selection + tool state
β”‚       └── types.zig
β”œβ”€β”€ commands/
β”‚   β”œβ”€β”€ lib.zig
β”‚   β”œβ”€β”€ CommandQueue.zig  undo/redo ring buffer
β”‚   β”œβ”€β”€ Dispatch.zig      Action β†’ handler router
β”‚   β”œβ”€β”€ handlers/         one file per command (AddWire, Move, Delete…)
β”‚   └── utils/            move, copy, delete helpers
β”œβ”€β”€ plugins/
β”‚   β”œβ”€β”€ PluginIF.zig      message protocol, ABI v6
β”‚   β”œβ”€β”€ Runtime.zig       load/tick/unload lifecycle
β”‚   β”œβ”€β”€ Framework.zig     helper abstractions for plugin authors
β”‚   └── installer/        plugin install/remove
β”œβ”€β”€ utility/
β”‚   β”œβ”€β”€ Vfs.zig           virtual filesystem (native + WASM)
β”‚   β”œβ”€β”€ Logger.zig        structured logging
β”‚   └── Platform.zig      OS abstraction
└── web/                  WASM-specific shell (IndexedDB VFS, boot.js)

Data Model

AppState vs Document

AppState (global, process-lifetime):

pub const AppState = struct {
    config:       Config,
    open_tabs:    ArrayList(Document),
    active_tab:   usize,
    plugins:      PluginHost,
    theme:        Theme,
};

Document (per-tab, schematic-lifetime):

pub const Document = struct {
    schematic:  Schematic,
    selection:  SelectionSet,
    tool_state: ToolState,   // wire mode, placement mode, etc.
    undo_queue: CommandQueue,
    file_path:  ?[]const u8,
    dirty:      bool,
};

This split means undo/redo is per-tab, plugins are global.

Schematic Data (DOD)

Schemify uses std.MultiArrayList (Structure-of-Arrays) for all schematic data.

pub const Schematic = struct {
    instances: MultiArrayList(Instance),
    wires:     MultiArrayList(Wire),
    nets:      MultiArrayList(Net),
    labels:    MultiArrayList(Text),
};

// Fast: iterating all X coords is contiguous memory
const xs = schematic.instances.items(.x);
const ys = schematic.instances.items(.y);

Core Types

pub const Wire = struct {
    x0, y0, x1, y1: i32,
    net_name: ?[]const u8,
    bus: bool,
};

pub const Instance = struct {
    name:        []const u8,   // "R1", "M2", "Xamp"
    symbol:      []const u8,   // reference to .chn_prim or .chn
    x, y:        i32,
    rot:         u2,           // 0=0Β°, 1=90Β°, 2=180Β°, 3=270Β°
    flip:        bool,
    kind:        DeviceKind,
    prop_start:  u32,          // index into flat property array
    prop_count:  u16,
    conn_start:  u32,          // index into flat connection array
    conn_count:  u16,
};

pub const Prop = struct { key, val: []const u8 };
pub const Conn = struct { pin, net: []const u8 };

Net connectivity uses union-find (NetMap): wires are merged into nets by scanning endpoints.

Command Queue and Undo

All mutations go through the Command union in commands/:

pub const UndoableAction = union(enum) {
    add_wire:            WireAddCmd,
    delete_selection:    DeleteCmd,
    move_instances:      MoveCmd,
    copy_paste:          PasteCmd,
    rotate_cw, rotate_ccw,
    flip_h, flip_v,
    set_instance_prop:   PropCmd,
    // ...
};

CommandQueue is a ring buffer β€” push appends, undo pops and inverts, redo re-applies.

// Trigger from GUI
actions.enqueue(app, .{ .undoable = .{ .add_wire = .{
    .start = p0,
    .end   = p1,
} } }, "Add wire");

Handlers in commands/handlers/ implement handle() and undo() for each action.

VFS β€” Virtual Filesystem

All file I/O goes through utility/Vfs.zig. Calling std.fs directly outside utility/ and cli/ is banned (enforced by lint).

Why: WASM has no native filesystem. VFS maps to:

  • Native: std.fs
  • Web: IndexedDB via JS interop
// In plugin or core code:
const data = try Vfs.readAlloc(allocator, "config.toml");
defer allocator.free(data);

try Vfs.writeAll("output.spice", netlist_text);
try Vfs.makePath("cache/pdk");

Rendering Pipeline

Frame order (back β†’ front):

  1. Grid
  2. Wires (WireRenderer)
  3. Instance bodies (SymbolRenderer)
  4. Instance pins
  5. Net labels
  6. Selection highlights (SelectionOverlay)
  7. Rubber-band rectangle
  8. Testbench ghost overlay (TbOverlay) β€” only when hovering TB pill
  9. Plugin overlays (floating panels)
  10. UI chrome (topbar, sidebar, toolbar, statusbar)

Testbench Overlay

When a .chn schematic is open and a matching .chn_tb exists in the same project:

  • Pill button appears in top-right of canvas: β–Ά test.chn_tb
  • Hover β†’ ghost-draws testbench wires on top of the DUT schematic (shows port connections)
  • Click β†’ switch active tab to the testbench
  • Shift+Click β†’ open testbench in a new tab

Implemented in gui/Canvas/TbOverlay.zig.

Dual Backend

zig build                    # native (SDL3 + Raylib, OpenGL)
zig build -Dbackend=web      # WASM (HTML5 Canvas via dvui wasm32 backend)

Same source. Platform-specific code isolated to web/ and the dvui backend selection in build.zig. VFS, plugin runtime, and all core logic are backend-agnostic.

CLI Mode

Schemify has a headless CLI mode β€” no display, no dvui:

zig build run -- --cli help
zig build run -- --cli netlist output.spice schematic.chn
zig build run -- --cli export-svg render.svg schematic.chn
zig build run -- --cli plugin-install ./libMyPlugin.so

Implemented in src/cli.zig. CLI mode compiles out the GUI entirely β€” no Xvfb needed in CI.